/**
 * Copyright 2008 Mathias Doenitz, http://lis.to/
 *
 * This file is part of the lis.to java desktop client. The lis.to java desktop client is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * The lis.to java desktop client is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with the lis.to java desktop client.
 * If not, see http://www.gnu.org/licenses/
 */

package listo.client;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.name.Names;
import listo.utils.CommandLineParser;
import listo.utils.MiscUtils;
import listo.utils.exceptions.Exceptions;
import listo.utils.guice.CustomScopes;
import listo.utils.guice.ManuallyScoped;
import listo.utils.guice.CalendarDayScoped;
import listo.utils.logging.ConsoleLog;
import listo.utils.logging.Log;
import listo.utils.logging.Log4JLog;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;

import javax.swing.*;
import java.io.File;
import java.io.FileWriter;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Application implements Thread.UncaughtExceptionHandler {

    private static final String DEFAULT_PREFS_FILE = "${HOME}/listo/listo.config.txt";

    public interface ShutdownListener {
        // Caution: the listener is called and run by the main thread
        public void onShutdown() throws Exception;
    }

    public static class UserException extends RuntimeException {
        public UserException(String message) {
            super(message);
        }
    }

    private static final Application instance = new Application();

    private Log log;
    private FileWriter lock;
    private final List<ShutdownListener> shutdownListeners = new CopyOnWriteArrayList<ShutdownListener>();

    private String prefsFile;
    private Preferences preferences;
    private Injector mainInjector;

    private final Object shutdownGate = new Object();
    private boolean shuttingdown;
    private Thread errorThread;
    private Throwable error;

    public static void main(String[] args) {
        instance.run(args);
    }

    private void setLookAndFeel() {
        try {
            log.debug("Setting system look and feel");
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch (Exception e) {
            throw new RuntimeException(String.format("Could not set system Look&Feel: %1$s\n", e.getMessage()));
        }
    }

    private void parseCommandLine(String[] commandLineArgs) {
        log.debug("Parsing command line [%s]", StringUtils.join(commandLineArgs, ' '));
        CommandLineParser cliParser = new CommandLineParser();
        CommandLineParser.Option prefsOption = cliParser.addStringOption('p', "prefs");

        try {
            cliParser.parse(commandLineArgs);
            prefsFile = (String) cliParser.getOptionValue(prefsOption, DEFAULT_PREFS_FILE);
        }
        catch (Exception e) {
            throw new UserException("Command line error: " + e.getMessage());
        }
    }

    private void loadPreferences() {
        preferences = Guice.createInjector(
                new AbstractModule() {
                    protected void configure() {
                        bind(Log.class).toInstance(log);
                        bindConstant().annotatedWith(Names.named("DEFAULT_PREFS_RESOURCE"))
                                .to("config/preferences.properties");
                        bindConstant().annotatedWith(Names.named("PREFS_FILE")).to(prefsFile);
                    }
                }
        ).getInstance(Preferences.class);
    }

    private void lockOnLockFile() {
        log.debug("Creating lock file");
        File file = new File(preferences.getLockFile());
        if (file.exists()) {
            if (!file.delete()) // if the file exists and we can't delete it there is another instance
            {
                log.error("Lock file already exists and can't be deleted");
                // no, we were not able to delete the lock file, so another instance is still active
                throw new UserException("You already have another lis.to instance running.\n" +
                        "(lis.to can only be started once.)");
            } else {
                log.warn("Lock file existed but could be deleted, previous application crash?");
            }
        }
        try {
            lock = new FileWriter(preferences.getLockFile());

            // write out a string that contains the process ID (in the sun VM)
            lock.write(ManagementFactory.getRuntimeMXBean().getName());
        }
        catch (Exception e) {
            throw new RuntimeException("Could not acquire write-lock on file " + preferences.getLockFile() + '\n' +
                    e.getMessage());
        }

        // add an exit handler that closes the lock and deletes the lock-file
        addShutdownListener(new ShutdownListener() {
            public void onShutdown() throws Exception {
                if (log != null) log.debug("Closing and deleting lock-file");
                lock.close();
                new File(preferences.getLockFile()).delete();
            }
        });
    }

    private void createMainInjector() {
        log.debug("Creating main guice injector");
        mainInjector = Guice.createInjector(
                new AbstractModule() {
                    protected void configure() {
                        bindConstant().annotatedWith(Names.named("CONFIG_RESOURCE")).to("config/config.properties");
                        bindConstant().annotatedWith(Names.named("DEFAULT_DATA_RESOURCE")).to("config/default.xml");
                        bindScope(ManuallyScoped.class, CustomScopes.MANUAL);
                        bindScope(CalendarDayScoped.class, CustomScopes.CALENDAR_DAY);
                        bind(Application.class).toInstance(Application.this);
                        bind(Preferences.class).toInstance(preferences);
                    }
                }
        );
    }

    private void run(String[] commandLineArgs) {
        try {
            log = new ConsoleLog();

            // first: try to set the system look & feel so that even the (potential) error messages look good
            setLookAndFeel();

            parseCommandLine(commandLineArgs);

            loadPreferences();

            lockOnLockFile();

            Log4JLog.configure("config/log4j.properties", preferences.getLogFile());

            createMainInjector();

            log.debug("Loading UI language resource bundle");
            mainInjector.getInstance(Lang.class).configure(preferences.getLanguage());

            log.debug("Configuring main logging infrastructure");
            log = mainInjector.getInstance(Log.class);

            System.setProperty("sun.awt.exception.handler", EDTErrorHandler.class.getName());
            Thread.setDefaultUncaughtExceptionHandler(this);

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    log.debug("Instantiating mainform dependencies and mainform");
                    MainForm mainForm = mainInjector.getInstance(MainForm.class);
                    mainForm.run();
                }
            });

            log.debug("Basic application initialization done, waiting for shutdown event");
            waitForShutdown();

            if (error == null) log.debug("Shutting down normally");

            // stop all background threads
            mainInjector.getInstance(Executors.class).shutdown();

            if (error != null) throw error;
        }
        catch (Throwable e) {
            handleApplicationException(e);
        }
        finally {
            log.debug("Calling shutdown hooks");
            for (ShutdownListener listener : shutdownListeners) {
                try {
                    listener.onShutdown();
                } catch (Exception e) {
                    handleApplicationException(e);
                }
            }
        }

        log.debug("Shutdown done, exiting after shutdown hooks complete");
        System.exit(0); // force exit, even if the EDT might still be alive
    }

    private void logError(Throwable e) {
        if (log != null) {
            log.error(String.format("Exception on %s: %s", errorThread != null ?
                    errorThread : Thread.currentThread(), Exceptions.getTrace(e)));
        }
    }

    private void handleApplicationException(Throwable ex) {
        try {
            logError(ex);

            errorThread = null; // reset for the potentially coming next exception on the main thread

            ex = Exceptions.getInnermost(ex);
            final String message = MiscUtils.wordWrap(
                    ex instanceof UserException ? ex.getMessage() :
                            "An application error has occurred. lis.to must close now.\n" +
                                    "We are very sorry for the inconvience.\n\nError details: " +
                                    (ex instanceof RuntimeException ? "" : ex.getClass().getName() + ": ") +
                                    ex.getMessage(), 80);

            final Object simpleLock = new Object();
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    log.debug("Displaying user error message popup");
                    JOptionPane.showMessageDialog(null, message, "lis.to Error", JOptionPane.ERROR_MESSAGE);

                    // wake up the main thread to continue the shutdown
                    synchronized (simpleLock) {
                        simpleLock.notify();
                    }
                }
            });

            synchronized (simpleLock) {
                simpleLock.wait();
            }
        }
        catch (Exception e) {
            logError(e);
        }
    }

    public void addShutdownListener(ShutdownListener listener) {
        shutdownListeners.add(listener);
    }

    /**
     * Blocks the current thread until shutdown() is called from another thread.
     *
     * @throws Exception when interrupted
     */
    private void waitForShutdown() throws Exception {
        synchronized (shutdownGate) {
            while (!shuttingdown) shutdownGate.wait();
        }
    }

    /**
     * Shuts down the application.
     */
    public void shutdown() {
        log.debug("Triggering application shutdown");
        shuttingdown = true;
        synchronized (shutdownGate) {
            shutdownGate.notify();
        }
    }

    /**
     * This class will be instantiated by the EventDispatchThread the first
     * time it encounters an exception or error.
     */
    public static class EDTErrorHandler {

        /**
         * This method is invoked by the AWT event dispatch mechanism when an
         * unexpected exception or error is thrown during event dispatching.
         *
         * @param t the exception
         */
        public void handle(Throwable t) {
            Application.instance.uncaughtException(Thread.currentThread(), t);
        }
    }

    /**
     * When an uncaughtException is raised on any thread then this method catches it.
     * Since the main thread is well protected the causing thread should be another thread,
     * e.g. the EDT or an executor thread. Probably the main thread is currently waiting
     * for the EDT to die so we wake it up to process the exception and shut down the application.
     *
     * @param t the thread
     * @param e the exception
     */
    public void uncaughtException(Thread t, Throwable e) {
        errorThread = t;
        error = e;
        shutdown();
    }

}
